Go-Design-Patterns-学习笔记 1 Go 语法入门

我又开始挖新坑啦,一直磨刀霍霍要学Go。变学边写,加深理解。之前粗略学了点 Golang 语法,现在借着这本书同时学习设计模式与 Golang。

书封面如下:

正文

Chapter 1: Ready…Steady…Go!

这章讲一些历史背景和 Golang 基本语法,也算对自己基础的巩固。

简史

缘起:

  • 在过去 20 年中,计算机科学经历了 incredible 增长。存储、内存都显著进步,CPU 也更快了。然而,相较存储和内存的增长而言,单个CPU 的进步却没有那么显著。实际上 CPU 工业的发展遇到了瓶颈,如果让单个 CPU 满功率地跑,散热已经不能满足了。
  • 为了解决这个问题,CPU 制造者开始在每台计算机上布置更多的 CPU 核心,以实现在满足散热的同时,增强计算机的计算能力。这种情形对一些系统编程语言变得挑战起来,尤其是那些一开始并不是为多核系统或分布式系统而设计的语言。比如 Google 意识到这越来越是个问题,尤其当他们挣扎于用 Java 或 C++ 这种一开始并不是为并发而设计的语言去开发分布式应用的时候。
  • 另一方面,现代程序越来越大、越来越复杂,维护起来越来越困难,同时容易出现坏的实践。我们的电脑有更多的核心以及越来越快,而我们在编写多核程序或者分布式应用时,并没有更快!

上帝说,要有 Golang:

  • Go 语言的设计开始于 2007 年,三个谷歌员工研究一种能够解决像谷歌这种大体量分布式系统中的通用问题的编程语言,这个三个员工是:
    • Rob Pike: Plan 9 and Inferno OS.
    • Robert Griesemer: Google V8 js engine that powers Google Chrome.
    • Ken Thompson: Worked at Bell labs and Unix team. involved in designing of Plan 9 OS and definition of the UTF-8 encoding.
  • 2008 年,编译器完成,团队获得 Russ Cox 和 Ian Lance Taylor 的支援
  • 2009 年,项目开源
  • 2012 年 3 月,经历 50 次小版本迭代后,发布了 1.0 版本

安装 Go

Go 的安装需要两个基本的东西:

  • 存放在硬盘上 Golang 二进制文件
  • 系统上中的 GOPATH 系统路径,用于存放工程文件以及从其他人那里下载的工程文件

Go 安装可能涉及三个系统 Linux、Window、OS X。我只关心 Linux:

  • 无障碍安装:

    • RHEL/Fedora/Centos users with YUM/DNF:

      1
      sudo yum install -y golang
    • Ubuntu/Debian users using APT with:

      1
      $ sudo apt-get install -y golang
  • 高级安装:
    Downloading the latest distribution from https://golang.org

Golang 的更新会维持向后兼容,所以更新版本时不用担心。书中推荐用高级方法安装,我就无障碍安装就好了。不过这里还是笔记一下 Linux 高级安装的相关信息:

  • 到官网 https://golang.org 下载 beta 版本的二进制安装包到下载文件夹中,比如保存为 tar.gz 文件

  • 解压,并放到合适的路径下,一般而言会放到 /usr/local/go 路径下,注意将下面第一条命令的 * 号替换为具体的版本号

    1
    2
    $ tar -zxvf go*.*.*.linux-amd64.tar.gz
    $ sudo mv go /usr/local/go
  • 添加 /usr/local/go/bin 到 PATH 中,比如在 ~/.bashrc 中,我用的是 ohmyzh,所以在 ~/.zshrc 中添加就好了

    1
    export PATH=$PATH:/usr/local/go/bin

设置 workspace

Go 程序都会在同一工作空间下运行,这让编译器可以方便地找到可能用到的相关包或库。这个工作空间叫作 GOPATH。

在开发 Go 程序的时候,GOPATH 是工作环境中一个很重要的角色。当你为你的代码 import 一个库时,它会在 $GOPATH/src 里面搜索这个库。同样,当你安装某些 Go 应用时,二进制文件会保存在 $GOPATH/bin 里。
同时需要注意,IDE源代码也需要保存在一个有效路径中,也就是 $GOPATH/src 文件夹中。举个栗子,我在 GitHub 中保存我的项目,我的用户名是 Sayden,一个项目名是 minimal-mesos-go-framework,那么我会用 $GOPATH/src/github.com/sayden/minimal-mesos-go-framework 这样的一个文件结构去反映这个代码仓库的 URL。

在本书中,我们这么去设置 GOPATH

1
$ mkdir -p $HOME/go

$HOME/go 这个路径会成为 $GOPATH 的目标路径。我们需要去设置一个环境变量只想这个文件夹,打开 $HOME/.bashrc (我用的是 zsh, 就打开 $HOME/.zshrc)。添加如下一行:

1
export GOPATH=${HOME}/go

使之生效:

1
source $HOME/.bashrc

或者

1
source $HOME/.zshrc

验证一下:

1
echo $GOPATH

Hello, world!

终于到了 hello, world 环节。在 $GOPATH/src/[your_name]/hello_world 中新建 main.go 文件:

1
2
3
4
5
package main

func main() {
println("Hello, World!")
}

运行:

1
$ go run main.go

输出:

1
Hello, World!

以上。go run [file] 会对源代码文件进行编译同时执行,但是不会生成可执行文件。如果你只是想编译并且得到一个可执行文件,可以用如下命令:

1
$ go build -o hello_world

得到一个 hello_world 文件,用命令可运行起来:

1
./hello_world

得到同之前同样的输出。这个可执行文件可以放在其他地方运行,不需要其他任何依赖。

开发环境

IDE (Intergrated Development Environment) 是一个用户界面,通过集成了一系列工具加速开发中一些常规流程,如 compiling、building、管理依赖等,从而帮助开发者编写代码。 

对于 Go 而言,专门面向 Golang 开发的 IDE 有 LiteIDE 和 Intellij Goland。其中 LiteIDE 功能较为单一,Goland 则强大很多。另外一些通用的 IDE 或者 编辑器有 Go 插件来帮助 Go 程序的开发。如:

  • IntelliJ Idea
  • Sublime Text 2/3
  • Atom
  • Eclipse
  • Vim
  • Visual Studio and Visual Code

类型 Types

类型 Types 赋予用户以一个方便好记的名字去存储数据。所有编程语言都有涉及到数字(存储整数、负数或者浮点数)、字符、字符串等等。Go 有如下常用的类型:

  • bool 布尔类型,存储 True 或者 False 状态
  • 数字类型:
    • int 整型,可表示的数字范围从 0 到 4294967295 (对于 32 位计算机)或 0 到 18446744073709551615 (对于 64 位机)
    • byte 可表示 0 到 255
    • float32 以及 float64 是所有 IEEE-754 64/-bit floating-point 数字集合
    • 对于 signed int,rune 是 int32 的别名,可以表示 -2147483648 到 2147483647,complex64 和 complex128 分别是所有 实部和虚部都是 float32 或 float64所有复数的集合
  • string 字符串型,表示一个字符序列,用引号括起来
  • array 数组内省,是具有固定长度,单一类型的有序元素序列
  • slice 切片类型,是对数组进行切分后的一个子集,和数组有些相似,但是使用更为方便和广泛
  • structures 结构类型,是不同类型元素的聚合
  • pointers 指针类型,是整型变量,记录所指向数据的内存地址,可以间接通过指针来修改查询某个对象的数据值
  • functions 函数类型,你可以定义函数类型为变量,作为参数值传入到其他函数中
  • interface 接口类型,一种非常重要的数据类型,提供了我们经常用到的封装和抽象功能
  • map 字面为映射,一种无序的 key-value 结构,需要说明的是,key 和 value 必须分别为统一数据类型
  • channels 通道类型,Go 语言中面向并发编程的重要数据类型,用于不同 go rountine 的通信

变量和常量 Variables and constants

变量是在计算机内存中用来存储数据的空间,在程序运行的过程中,他的值可以被改变。这里有两种定义变量的方式,第一种是显式,第二种是隐式

1
2
3
4
// 显式
var explicit string = "Hello, I'm a explicitly declared variable"
// 隐式
infered := ", I'm an inferred vairalble"

通过 reflect 包可以判断变量的类型,main.go 完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"reflect"
)

func main(){
println("Hello World!")
var explicit string = "Hello, I'm a explicitly declared variable"
inferred := "Hello, I'm an inferred vairalble"
fmt.Println(explicit)
fmt.Println(inferred)
fmt.Println("variable 'explicit' is of type: ",
reflect.TypeOf(explicit))
fmt.Println("Variable 'inferred' is of type: ",
reflect.TypeOf(inferred))
}

然后用 go run main.go 获得结果:

1
2
3
4
5
Hello World!
Hello, I'm a explicitly declared variable
Hello, I'm an inferred vairalble
variable 'explicit' is of type: string
Variable 'inferred' is of type: string

操作符 Operators

操作符被用于进行算术运算或这多个对象的比较。如下操作符是 Go 的保留操作符:

1 2 3 4 5 6 7 8 9
+ & += &= && == != ( )
- | -= |= || < <= [ ]
* ^ *= ^= <- > >= { }
/ << /= <<= ++ = := , ;
% >> %= >>= ! . :
&^ &^=

算术运算符:

  • + 求和运算
  • - 求差运算
  • * 乘法运算
  • / 除法运算
  • % 求余运算
  • ++ 加 1 运算
  • – 减 1 运算

比较运算符:

  • == 是否相等
  • != 是否不等
  • > 左边是否大于右边
  • < 左边是否小于右边
  • >= 左边是否大于或等于右边
  • <= 左边是否小于或等于右边
  • && 两个值是否都为真

除此之外,还有一些设计到对二进制数进行操作的操作符,在后面会详细讲解到。

流程控制 Flow control

引用原文吧,更准确一些:

Flow control is referred as the ability to decide which portion of code or how many times
you execute some code on a condition. In Go, it is implemented using familiar imperative
clauses like if, else, switch and for.

if… else

和其他语言中的 if…else 类似,不过这里 if 后面的布尔表达式不需要括号,如果为真的执行代码需要大括号。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

func main(){
ten := 10
if ten == 20 {
println("This shouldn't be printed as 10 isn't equal to 20")
} else {
println("Ten is not equals to 20");
}
if "a" == "b" || 10 == 10 || true == false {
println("10 is equal to 10")
} else if 11 == 11 && "go" == "go" {
println("This isn't print because previous condition was satisfied");
} else {
println("In case no condition is satisfied , print this")
}
}

switch

和其他语言如 C++ 类似,switch 用来控制多个分支逻辑。python 没有这个语法,要等效实现这个功能,需要连续使用多个 if 代替。用 switch 确实会让代码清爽很多,不过需要多保留两个关键字,各有利弊吧。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

func main(){
number := 3
switch(number) {
case 1:
println("Number is 1")
case 2:
println("Number is 2")
case 3:
println("Number is 3")
}
}

for…

for 用来控制循环逻辑。和 if 类似,条件语句不需要括号,不过如果涉及多条语句(初始化,运行条件,单词循环后的处理)需要用分号隔开。
示例:

1
2
3
4
5
6
7
package main

func main(){
for i := 0; i <= 10; i++ {
println(i)
}
}

针对像数组或者切片这种可迭代对象,for 有特殊语法。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main(){
my_array := [3] int {1, 2, 3}
for index, value := range my_array {
fmt.Printf("index is %d and value is %d \n", index, value)
}
}

此外,对于无限循环 while,也是用 for 关键字进行表示,不过不用写条件语句了。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main(){
end := 50
start := 0
for {
fmt.Printf("the value of this cycle is %d \n", start)
start++
if start == end {
break
}
}
}

函数 Functions

对函数的阐释引用原文吧:

A function is a small portion of code that surrounds some action you want to perform and
returns one or more values (or nothing). They are the main tool for developer to maintain
structure, encapsulation, and code readability but also allow an experienced programmer to
develop proper unit tests against his or her functions.

Functions can be very simple or incredibly complex. Usually, you’ll find that simpler
functions are also easier to maintain, test and debug. There is also a very good advice in
computer science world that says: A function must do just one thing, but it must do it damn well.

在 go 中,函数有如下结构:

func [function_name] (param1 type, param2 type …) (returend type1, returend type2…){
//Function Body
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main(){
message := "Clay"
hello(message)
}

func hello(message string) error {
fmt.Printf("Hello, %s! \n", message)
return nil
}

此外,函数也可以作为其他函数的输入参数或者返回值。编写函数时,对函数名要慎重考虑,以利于调用者可以通过函数名对函数功能或一些特性能猜个大概。编写函数时,除了满足预期功能,也要充分考虑单元测试,同时让代码简洁明了。

匿名函数 anonymous function

匿名函数在很多语言中都有,个人理解是适合那种即写即用的短小函数。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main(){
add := func(m int) int {
return m+1
}
result := add(6)
fmt.Printf("the result is %d \n", result)
}

这个 add 函数只能在 main 中使用。匿名函数在设计模式中是一个强有力工具,会经常用到。

闭包 Closures

闭包和匿名函数比较类似,但更为强大。同匿名函数相比,最关键的不同之处是,匿名函数内部没有上下文背景,但是闭包有。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main(){
addN := func (m int) func(int) int {
return func(n int) int {
return m+n
}
}

addFive := addN(5)
result := addFive(6)
fmt.Printf("result is %d \n", result)
}

创建错误、处理错误、返回错误

错误在 Go 中有大量应用,大概是因为创建一个错误声明太简单了吧。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"errors"
)

func main(){
err := doesReturnError()
if err != nil {
panic(err)
}
}

func doesReturnError() error {
err := errors.New("this function simply returns an error")
return err
}

不定数量参数

某些时候,函数参数个数并不一定能够提前确定下来。这个时候,需要函数允许接收不定数目的参数。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main(){
fmt.Printf("%d\n", sum(1, 2, 3))
fmt.Printf("%d\n", sum(4, 5, 6, 7, 8))
}

func sum(args ...int) (result int) {
for _, v := range args {
result += v
}
return
}

对返回类型命名

一般而言,我们对函数会按如下方式进行声明:

func sum(args …int) int

但是在上一个例子中,我们写成:

func sum(args …int) (result int)

这个实际上进行了变量声明,并赋予零值。在函数结束时,只需要 return 而不需要 return result。Go 会把函数中需要返回的变量按照之前定义的顺序一并返回。

数组、切片、maps Arrays, slices, maps

数组 arrays 是一个具有固定长度,单一数据类型的有序序列,可以通过位置编号进行访问,在计算机编程中被广泛使用。数组唯一缺陷是它的长度不能变化。切片 slices 允许变长的对数组进行使用。映射 maps 让我们可以在 go 中使用类似字典那种数据结构。

数组 arrays

两种声明方式:

1
var arr [100] int

or

1
arr := [3] string{"go", "is", "awesome"}

零初始化

在声明变量的时候,如果没有显示赋值,go 会对变量依据其数据类型进行零初始化。布尔零值为 false,字符串为空字符串,整形为 0,如此这般。

切片 Slices

Slices 和数组很类似,可以在运行过程中更改大小。Slices 的底层就是数组。所以,类似声明数组,我们需要指明 Slices 的元素类型以及他的大小。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
)
func main(){
// 声明一个 slice,初始化后所有值都是 0
mySlice := make([]int, 10)
for index, value := range(mySlice) {
fmt.Printf("index is %d, value is %d \n", index, value)
}
fmt.Printf("\n")

// 追加一个数,总个数为 11,前面 10 个是 0, 最后一个是 5
mySlice = append(mySlice, 5)
for index, value := range(mySlice) {
fmt.Printf("index is %d, value is %d \n", index, value)
}
fmt.Printf("\n")

// 去掉第一个,得到新 slices
mySlice = mySlice[1:]
for index, value := range(mySlice) {
fmt.Printf("index is %d, value is %d \n", index, value)
}
fmt.Printf("\n")

// 去掉第二个值
mySlice = append(mySlice[:1], mySlice[2:]...)
for index, value := range(mySlice) {
fmt.Printf("index is %d, value is %d \n", index, value)
}
fmt.Printf("\n")
}

Maps

类似字典类型,可以用 string 指向 numbers、interfaces、structs, 不过 slices, functions, 和 maps 本身不能作为 key。
示例:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
)
func main(){
myMap := make(map[string]int)
myMap["one"] = 1
myMap["two"] = 2
fmt.Println(myMap["one"])
}

当需要对 json 数据进行解析时,可以用如下 map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"encoding/json"
)
func main(){
myJsonMap := make(map[string]interface{})
jsonData := []byte(`{"hello": "world", "age": 5.0}`)
err := json.Unmarshal(jsonData, &myJsonMap)
if err != nil {
panic(err)
}
fmt.Printf("%s \n", myJsonMap["hello"])
fmt.Println(myJsonMap["age"])
}

Json 数据的 value 值有可能是各种不同类型的数据类型,这里用 interface{} 空接口来表示不定格式的数据类型。这里 json.Unmarshal 是用来解析json数据的,不过需要把map 的指针作为参数传入 Unmarshal 中作为承接 json 解析后出来的数据。需要注意的是,这里在查询解析后的数据详情时,需要判断解析过程中是否有错误。

可见性 Visibility

这里可见性是指,函数或者变量是否对程序的其他部分可见(可访问)。可以去控制一个变量是否仅仅在它申明的所在函数可见,或者在所在包中可见,或者在整个程序可见。怎么实现呢?下面的方法一开始可能会让人困惑,但是会简单很多:

首字母大写可以让变量或者函数是公开的,或者说在整个程序中可访问
首字母小写指私有,只在所在函数内部可见,不可跨包

public 示例:
在 main.go 同级新建一个 hello 文件夹,创建 hello.go

1
2
3
4
5
package hello 

func Hello_world() {
println("Hello World!")
}

main.go 代码为:

1
2
3
4
5
6
7
8
package main

import (
"clay/hello_world/hello"
)
func main(){
hello.Hello_world()
}

这里要十分注意 import 的路径,是以 $GoPath/src 作为根目录的。我这里 echo $GOPATH是 /home/clay/go, 而 hello.go 的绝对路径是 /home/clay/go/src/clay/hello_world/hello/hello.go

如上,Hello_world 是一个全局函数,凡导入 hello 包的地方都可以访问这个函数。

零初始化 Zero-initialization

零初始化有些时候会导致一些困惑。它是指你在声明各种类型的变量时,没有提供出事值,而 go 默认给这些变量提供了初始值。对不同类型的零初始化如下:

类型 零初始值
bool false
int 0
float 0.0
string “”
pointers nil
functions nil
interfaces nil
slices nil
channels nil
maps nil
struct 针对每个字段零初始化,如果没有字段则是空 struct

零初始化在 go 编程中很重要,比如你需要返回一个 int 或 struct 类型的话,你不能返回一个 nil。先看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
)

func main(){
res := divisibleBy(10, 0)
fmt.Printf("%v \n", res)
}

func divisibleBy(n, divisor int) bool {
if divisor == 0 {
return false
}

return (n % divisor == 0)
}

输出结果是 false 但是这是不对的,当出现除数为 0 的时候需要报错才行。10 并不是不能被 0 整除,而是 0 本身就不能作为除数。如何修改呢?看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"log"
"errors"
)

func main(){
res, err := divisibleBy(10, 0)
if err != nil {
log.Fatal(err)
}
log.Printf("%v \n", res)
}

func divisibleBy(n, divisor int) (bool, error) {
if divisor == 0 {
return false, errors.New("A number cannot be divided by zero")
}

return (n % divisor == 0), nil
}

指针和结构体 Pointers and structures

对于 C 或 C++ 程序员而言,指针真是令人头疼的原因之一。但是在没有垃圾回收的语言中,指针是实现高性能代码的主要工具之一。遇上 go,你就幸运多了,go 作了最好实现,同时兼顾到了高性能指针、垃圾回收、易用性。

对于不喜欢 Go 语言的人而言,GO 缺少继承特性而更倾向于用组合方式。在 Go 中,我们不会说某些对象是什么,而是说那些对象都有什么(对象)。比如,我们不会说一个 car 结构体继承于 vehicle 类,而是说一个 vehicle 结构体包含一个 car 结构体。

什么是指针,为什么它们好用呢?

指针让人又爱又恨:

Pointers are hated, loved, and very useful at the same time.

指针和变量的关系,如果楼下一排排邮箱同房子的关系,房子大小、户型、用途都有不能,但是都有相同大小的邮箱。如果房子想接收一些东西时,只用房子地址(邮箱)去接收比把房子搬过去接收要高效的多。当然风险也是有的,比如房子消失了,或者所有者发生了变化。

举个栗子,比如在一个变量中你有 4GB 变量,你需要把它传递到不同的函数。不使用指针的话,所有数据将被复制到函数中间来使用。复制一次,就会占用 8GB 内存,多个函数将占用内存成倍增加。

同 C 或 C++相比,指针在 Go 中的使用还是比较克制的。

1
2
3
4
5
6
7
8
package main

func main(){
number := 5
ponter_to_number := &number
println(ponter_to_number)
println(*ponter_to_number)
}

结构体 Structs

在 Go 中,一个结构体就是一个对象。定义一个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
)

type Person struct {
Name string
Surname string
Hobbies []string
id string
}

func (person *Person) GetFullName() string {
return fmt.Sprintf("%s %s", person.Name, person.Surname)
}

func main(){
p := Person{
Name: "Mario",
Surname: "Castro",
Hobbies: []string{"cycling", "electronics", "planes"},
id: "sa3-223-asd",
}
fmt.Printf("%s likes %s, %s and %s \n", p.GetFullName(), p.Hobbies[0],
p.Hobbies[1], p.Hobbies[2])
}

这里在定义结构体的方法时,用了 (person *Person)。这里类似 python 中的 self,代表的是把某个对象的指针传到函数中来。如果不用指针,而用 (person *Person) 则会把对象完整拷贝过来,原对象发生改变时,函数中的值不会更随改变。

接口 Interfaces

接口在面向对象编程、函数式编程很重要,尤其是在设计模式中。Go 的源代码中充满了接口,因为在函数的帮助下,这些接口提供了解耦代码所需要的抽象。

掌握接口在一开始会比较困难,但是你一旦明白了它的行为模式就会非常容易,接口为一些常用问题提供了非常优美的解决方案。我们在本书中也会大量用到,所以需要对它要多花点功夫。

接口 - 签一个合同

接口非常简单,却又非常强大。它经常被当做对象间签署的一个合同。对初学者而言,比喻成合同或许有些不清楚。我们换个比喻吧。

其实一个水管也是一个合同,无论你往里面通过什么,它必须是流体。任何人都可以用这个水管,管子呢也会让任何放进去的流体通过(不需要知道通过的流体具体是什么内容)。水管就是使用者之间传输流体的一个接口。

另外一个例子是铁轨和火车的关系,只要火车车轮的几何尺寸和铁轨相同,那么这个火车就可以在铁轨上面跑,而铁轨不需要知道这个火车是拉的货还是人。

示例: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
)

type RailroadWideChecker interface {
CheckRailsWidth() int
}

type Railroad struct {
Width int
}

func (r *Railroad) IsCorrectSizeTrain(train RailroadWideChecker) bool {
return train.CheckRailsWidth() == r.Width
}

type Train struct {
TrainWidth int
}

func (p *Train) CheckRailsWidth() int {
return p.TrainWidth
}

func main() {
railroad := Railroad{
Width: 10,
}
passengerTrain := Train {
TrainWidth: 10,
}
cargoTrain := Train {
TrainWidth: 15,
}
canPassengerTrainPass := railroad.IsCorrectSizeTrain(&passengerTrain)
canCargoTrainPass := railroad.IsCorrectSizeTrain(&cargoTrain)

fmt.Printf("Can passenger train pass? %t \n", canPassengerTrainPass)
fmt.Printf("Can cargo train pass? %t \n", canCargoTrainPass)
}

测试和测试驱动开发 Testing and TDD

测试对于复制系统的开发很重要,除了验证新加代码的功能外,也需要考察新代码是否对原有功能有破坏作用。

Go 语言有一个强悍的测试包,允许我们较为容易进行测试驱动开发。

测试包

在所有语言中,测试都是非常重要的。Go 语言的设计者知道这一点,决定在核心源代码中提供测试所需要的所有库和包。你不需要任何第三方包来进行测试。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"strconv"
"os"
)

func main() {
// Atoi converts a string to an int
a, _ := strconv.Atoi(os.Args[1])
b, _ := strconv.Atoi(os.Args[2])

result := sum(a, b)
fmt.Printf("The sum of %d and %d is %d \n", a, b, result)
}

func sum(a, b int) int {
return a + b
}

上述代码保存在,main.go 中,测试代码需要保存在另一个同级文件 main_test.go 中。
测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main 

import (
"testing"
)

func TestSum(t *testing.T) {
a := 5
b := 6
expected := 11

res := sum(a, b)
if res != expected {
t.Errorf("Our sum function doesn't work, %d + %d isn't %d \n", a, b, res)
}
}

用 go test -v 或者 go test -cover 进行测试。这里 -v 会输出测试中的过程, -cover 会输出对给定包的覆盖率(不幸的是,不会提供整个应用的测试覆盖率)

TDD 是啥?

TDD 是 Test Driven Development 的首字母缩写。它强调的是在写功能代码之前先写测试代码。

TDD 改变了编写代码,以及代码结构,以便这些代码是可以测试的。那它是怎么工作的呢?设想一下,在夏天里,你想做些清理工作。你可以建造一个游泳池,向里面注慢凉水,并跳进去。但是在在 TDD 中,你的步骤是这样的:

  • 你跳进一个地方,在那里游泳池将要被建造(如果写个测试,你知道测试不会通过)
  • 你会受伤,并且你也不会变得凉快(意料之中,测试不会通过)
  • 你建造一个泳池,并注满凉水(编写功能代码)
  • 你跳入泳池(重复第一步的测试)
  • 你变凉快了吧,haha!任务完成(测试通过)
  • 从冰箱里拿瓶啤酒到泳池里,喝(代码重构)

让我们重复之前的例子,不过这次我们写乘法。首先我们需要写我们将要测试的函数声明。

1
2
3
4
5
package main

func multiply(a, b int) int {
return 0
}

写测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main 

import (
"testing"
)

func Test(t *testing.T) {
a := 5
b := 6
expected := 30

res := multiply(a, b)
if res != expected {
t.Errorf("Our multiply function doesn't work, %d + %d isn't %d \n", a, b, res)
}
}

运行 go test -v
结果是不通过。

修改函数代码:

1
2
3
4
5
package main

func multiply(a, b int) int {
return a*b
}

运行 go test -v,测试通过。
在本书中,我们会写许多测试去定义在设计模式中我们想要实现的功能。

库 Libraries

到此位置,我们所有案例都是应用,这个应用被 main 函数及其他包所定义。在 Go 中,你也可以定义纯库,也就是可以没有 main 函数。既然库不是应用,你也就不能构建二进制文件,如果要使用它们,你需要 main 包。

这里,我们打算创建一个 arithmetic 包,包中所有文件的包名都是 arithmetic。这样,在需要使用包中功能时,我们只需要提供路径和包名即可,而不需要指向到某个特定文件。个人理解,这种对包处理的好处是,路径可以兼顾外围的操作系统路径,同时对内部功能模块用逻辑路径管理。

我们新建一个文件夹 arithmetic,并创建 arithmetic.go,编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package arithmetic

import (
"errors"
)

func Sum(args ...int) (res int) {
for _, v := range args {
res += v
}
return
}

func Subtract(args ...int) int {
if len(args) < 2{
return 0
}
res := args[0]
for i :=1; i < len(args); i++ {
res -= args[i]
}
return res
}

func Multiply(args ...int) int {
if len(args) < 2 {
return 0
}

res := 1
for i:=0; i < len(args); i++ {
res *= args[i]
}
return res
}

func Divide(a, b int) (float64, error) {
if b == 0 {
return 0, errors.New("You cannot divide by zero")
}
return float64(a) / float64(b), nil
}

由于 arthmetic 本身没有 main 函数,我们可以写测试代码测试,arithmetic_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package arithmetic

import (
"testing"
)

func TestMultiply(t *testing.T){
a := 5
b := 6
expected := 30

res := Multiply(a, b)
if res != expected {
t.Errorf("Our multiply function doesn't work, %d * %d isn't %d \n", a, b, res)
}

a = 5
expected = 0
res = Multiply(a)
if res != expected {
t.Errorf("Our multiply function doesn't work, %d isn't %d \n", a, res)
}
}

func TestSum(t *testing.T){
a := 5
b := 6
expected := 11

res := Sum(a, b)
if res != expected {
t.Errorf("Our Sum function doesn't work, %d + %d isn't %d \n", a, b, res)
}
}

func TestSubtract(t *testing.T){
a := 11
b := 5
expected := 6

res := Subtract(11, 5)
if res != expected {
t.Errorf("Our Subtract function doesn't work, %d - %d isn't %d \n", a, b, res)
}

a = 5
expected = 0
res = Subtract(a)
if res != expected {
t.Errorf("Our Subtract function doesn't work, %d isn't %d \n", a, res)
}
}


func TestDivide(t *testing.T){
a := 30
b := 5
expected := 6

res, err := Divide(a, b)
if err != nil {
if res != float64(expected) {
t.Errorf("Our Divide function doesn't work, %d - %d isn't %f \n", a, b, res)
}
}

a = 30
b = 0
expected = 0
res, err = Divide(a, b)
if err == nil {
t.Errorf("Our Divide function doesn't work, %d - %d isn't %f \n", a, b, res)
} else if res != float64(expected) {
t.Errorf("Our Divide function doesn't work, %d - %d isn't %f \n", a, b, res)
}
}

Go get 工具 The Go get tool

JSON 数据管理 Managing JSON data

The encoding package

Go 工具 Go tools

The golint tool

The gofmt tool

gofmt main.go
gofmt -w main.go

The godoc tool

go doc encoding/json
godoc -http=localhost:6060
go doc encoding/json | grep Unmarshal

The goimport tool

自动化按需 import

在 Github 上为 Go 开源项目贡献代码

要注意 go get 与 git 的配合使用。

小结

马马虎虎,这章算看完了,后面的几个小结就不做笔记了,到时候用的时候再巩固一下。

总结

Golang 的基本语法除了并发那块,算学完了。总计想起来,代码还是要使劲敲才能熟练。加油吧!